/// <reference path="HostBridge.ICE.ts"/>
/// <reference path="JQuery\jquery.d.ts"/>
/// <reference path="Linq\linq.d.ts"/>
/// <reference path="Linq\linq.jquery.d.ts"/>
/// ...

// ===========================================================================================================================

if (typeof host == "undefined")
    host = new __NonDreamSpaceHost_ICE__();

var $ICE = <__NonDreamSpaceHost_ICE__>host;

//(only 'jQuery.Enumerable' will exist because only "jquery.linq.js" is included in the default HTML file (contains linq.js embedded),
// so this is needed to be compatible with "linq.d.ts".  That said, 'this.Enumerable' will exist in the server environment because
// only "linq.js" will be loaded)
if (!this.Enumerable && this.jQuery && jQuery.Enumerable)
    Enumerable = jQuery.Enumerable;

//??declare var $_DS: DreamSpace.DreamSpaceSystem;

// ===========================================================================================================================

// The DreamSpace Module
module DreamSpace {

    export var ___functions: any[] = [];

    /** Detects and stores function names for the function objects, because TypeScript uses anonymous functions for class members. */
    export var setFunctionNames = (obj: Object, funcName: string, fullName: string = null, levelMax: number = 4) => {
        var p: string, fb, o = funcName != null ? obj[funcName] : obj;
        if (funcName)
            if (fullName) fullName += "." + funcName; else fullName = funcName;
        if (o != null && funcName != null) {
            fb = o.toString(); //.replace(" ", "_");
            //if (fb in ___functions) return; // already exists, so exit (if o==null then this is a prototype object)
            ___functions[fb] = fullName;
        }
        //!!if (funcName == "GraphItem") debugger;
        if (levelMax == 0) return; // (limit the number of nested levels to processes)
        levelMax--;
        for (p in o)
            if (o.hasOwnProperty(p))
                if (typeof o[p] == "function")
                    setFunctionNames(o, p, fullName, levelMax); // (static functions)
                else if (p.charAt(0) >= 'A' && p.charAt(0) <= 'Z')
                    setFunctionNames(o[p], null, fullName + "." + p, levelMax); // (this is a "type" object because of the first uppercase character)
        if (o.prototype)
            setFunctionNames(o.prototype, null, fullName, levelMax); // (prototype functions)
    };

    export var printStackTrace = () => {
        var callstack = [];
        var isCallstackPopulated = false;
        try {
            throw "";
        } catch (e) {
            if (e.stack) { //Firefox
                var lines = e.stack.split('\n');
                for (var i = 0, len = lines.length; i < len; i++) {
                    if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
                        callstack.push(lines[i]);
                    }
                }
                //Remove call to printStackTrace()
                callstack.shift();
                isCallstackPopulated = true;
            }
            else if (window["opera"] && e.message) { //Opera
                var lines = e.message.split('\n');
                for (var i = 0, len = lines.length; i < len; i++) {
                    if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
                        var entry = lines[i];
                        //Append next line also since it has the file info
                        if (lines[i + 1]) {
                            entry += ' at ' + lines[i + 1];
                            i++;
                        }
                        callstack.push(entry);
                    }
                }
                //Remove call to printStackTrace()
                callstack.shift();
                isCallstackPopulated = true;
            }
        }
        if (!isCallstackPopulated) { //IE and Safari
            var currentFunction = arguments.callee.caller;
            while (currentFunction) {
                var fn = currentFunction.toString();
                var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf('')) || 'anonymous';
                callstack.push(fname);
                currentFunction = currentFunction.caller;
            }
        }
        return callstack;
    }

    /** A more informative way to generate exception errors. */
    export var Exception = (msg: string): string => {
        var callerFunction = DreamSpace.Exception.caller.toString();
        var callerName = DreamSpace.___functions[callerFunction];
        //var srcName = callerFunction.substring(callerFunction.indexOf("function"), callerFunction.indexOf("("));
        var args = DreamSpace.Exception.caller.arguments;
        var _args = args && args.length > 0 ? Array.prototype.join.call(args, ', ') : "";
        msg = (callerName ? "[" + callerName + "(" + _args + ")]" : "") + ": " + msg + "\r\n\r\n";
        msg += callerFunction + "\r\n\r\nStack:\r\n";
        var caller = DreamSpace.Exception.caller.caller;
        while (caller) {
            callerFunction = caller.toString();
            callerName = DreamSpace.___functions[callerFunction];
            args = caller.arguments;
            _args = args && args.length > 0 ? Array.prototype.join.call(args, ', ') : "";
            //srcName = callerFunction.substring(callerFunction.indexOf("function"), callerFunction.indexOf(")") + 1);
            if (callerName)
                msg += callerName + "(" + _args + ")\r\n";
            else
                msg += callerFunction + (_args ? " using arguments (" + _args + ")" : "") + "\r\n";
            caller = caller.caller != caller ? caller.caller : null; // (make sure not to fall into an infinite loop)
        }
        return msg;
    };

    /** Called when the DreamSpace system is ready for use.  The system should not be accessed until this method is called. */
    export var onReady: Function = null;

    /** Sets the debug mode.
    * A developer should set this to true to force debugging when running without a host bridge (usually when running in a web browser).
    * If this is true, then 'DreamSpace.run()' is never called.
    */
    export var debug: boolean = false;

    // =======================================================================================================================

    export interface PropertyChangedHandler { (item: GraphItem, name: string, value: any): void; }

    // =======================================================================================================================

    /** A graph item represents a single node on the application graph. */
    export class GraphItem {

        // -------------------------------------------------------------------------------------------------------------------

        /** Returns a default accessor function to be used when creating accessor functions in GraphItem derived types. */
        static accessor = (observablePropertyName: string): any => {
            return function (value) {
                if (arguments.length > 0)
                    return (<GraphItem>this).setProperty(observablePropertyName, value);
                return (<GraphItem>this).getProperty(observablePropertyName);
            };
        };

        // -------------------------------------------------------------------------------------------------------------------

        static defaultHTMLTag: string = "div";

        static nextID: number = 0;

        // -------------------------------------------------------------------------------------------------------------------

        id: number = -1;
        name: string = "";

        parent: GraphItem = null;
        children: Array<GraphItem> = [];

        /** Represents the HTML element tag to use for this graph item.  The default value is set when a derived graph item is constructed.
        * Not all objects support this property, and changing it is only valid BEFORE the layout is updated for the first time.
        */
        htmlTag: string = GraphItem.defaultHTMLTag;
        element: Node = null;
        htmlElement: HTMLElement = null;

        /** Holds a collection of properties, indexed by property name, that are to be stored with this graph item. */
        properties: any[] = [];

        /**
        * Holds a copy of the 'properties' collection before the application starts.
        * This allows quick restarting the application later if desired.
        */
        initialProperties: any[] = null;

        /** Holds a list of registered handlers to be notified when one or more properties update. */
        propertyChangedHandlers: PropertyChangedHandler[] = null;

        // -------------------------------------------------------------------------------------------------------------------

        constructor(parent: GraphItem = null) {
            this.id = GraphItem.nextID++;
            if (parent)
                parent.addChild(this);
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** This is called internally in the layout phase upon creating elements for the first time. */
        updateInitialProperties() {
            this.initialProperties = [];
            for (var p in this.properties)
                this.initialProperties[p] = this.properties[p];
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** Sets an observable property value on the graph item object.
        * Observable properties can trigger events to allow the UI to update as required.
        * @see also: {@link getProperty}
        */
        setProperty(name: string, value: any, triggerChangeEvent: bool = true): any {
            this.properties[name] = value;

            // ... settings a local observable property will also set any corresponding element property ...
            if (this.element && (<HTMLElement>this.element).setAttribute)
                this.htmlElement.setAttribute(name, value);

            if (triggerChangeEvent)
                this._DoOnPropertyChanged(name, value);

            return value;
        }

        /** Gets an observable property value from the graph item object.
        * Observable properties can trigger events to allow the UI to update as required.
        * @see also: {@link setProperty}
        */
        getProperty(name: string): any {
            return this.properties[name];
        }

        private _DoOnPropertyChanged(name: string, value: any) {
            if (!this.initialProperties)
                return; // (don't trigger events until the layout has completed)

            if (this.onPropertyChanged)
                this.onPropertyChanged(name, value);

            var i;
            if (this.propertyChangedHandlers != null)
                for (i = 0; i < this.propertyChangedHandlers.length; i++)
                    this.propertyChangedHandlers[i](this, name, value);
        }

        /** Called when an observable property changes.  The default implementation looks for 'PropertyName+"UIProperty"' in the static scope, and if exists,
        * auto triggers a redraw event (in which 'onRedraw()' is called).
        * Derived types should override this if needed, and optionally call 'super.onPropertyChanged(name, value)' to maintain existing behavior.
        * TIP: If this will never be used by derived types, this function property can be set to 'null' to ignore it (more efficient).
        */
        onPropertyChanged(name: string, value: any) {
            if ("UIProperties" in (<any>this).constructor && name in (<any>this).constructor.UIProperties)
                this.onRedraw();
        }


        // -------------------------------------------------------------------------------------------------------------------

        /** Adds a handler that will be called when a property is added, modified, or deleted.
        * @see also: {@link removePropertyChangedHandler}
        */
        addPropertyChangedHandler(handler: PropertyChangedHandler) {
            if (!this.propertyChangedHandlers)
                this.propertyChangedHandlers = [];
            var i = this.propertyChangedHandlers.indexOf(handler);
            if (i == -1) this.propertyChangedHandlers.push(handler);
        }

        /** Removes property change handlers.
        * @see also: {@link addPropertyChangedHandler}
        */
        removePropertyChangedHandler(handler: PropertyChangedHandler) {
            var i;
            if (this.propertyChangedHandlers)
            {
                i = this.propertyChangedHandlers.indexOf(handler);
                if (i > 0) this.propertyChangedHandlers.splice(i, 1);
            }
        }

        // -------------------------------------------------------------------------------------------------------------------

        addChild(item: GraphItem): GraphItem {
            // ... make sure the item doesn't exist, and move it from any other parent ...
            var i = this.children.indexOf(item);
            if (i == -1) {
                if (item.parent != null)
                    item.parent.removeChild(item);
                item.parent = this;
                this.children.push(item);
            }
            return item;
        }

        removeChild(item: GraphItem): GraphItem {
            if (item) {
                var i = this.children.indexOf(item);
                if (i >= 0) this.children.splice(i, 1);
                item.parent = null;
            }
            return item;
        }

        removeChildAt(itemIndex: number): GraphItem {
            if (itemIndex >= 0) return this.children.splice(itemIndex, 1)[0];
            return null;
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** The function is called by the system in order to generate/update the required HTML elements for display.
        * @param {bool} recursive Set to false to only update this graph item, ignoring all children.
        */
        updateLayout(recursive: bool = true) {

            // ... create this item's element before the child items get updated ...

            var parentElement: Node = this.parent ? this.parent.element : null;
            var i: number;

            if (this.element == null || this.element.nodeName != this.htmlTag) {
                if (this.element != null && parentElement != null)
                    parentElement.removeChild(this.element);

                this.element = this.createUIElement();
                if (this.element instanceof HTMLElement)
                    this.htmlElement = <HTMLElement>this.element;
                else
                    this.htmlElement = null;

                if (this.element != null && parentElement != null)
                    parentElement.appendChild(this.element);

                if (this.initialProperties == null)
                    this.updateInitialProperties();
            }
            else if (parentElement != null && this.element.parentNode != parentElement)
            {
                // ... the parent element is different for the existing element, so *move* it to the new parent ...
                this.element.parentNode.removeChild(this.element);
                parentElement.appendChild(this.element);
            }

            // ... update the deepest items first (this is a logical pass only) ...

            if (recursive)
                for (i = 0; i < this.children.length; i++) {
                    this.children[i].updateLayout(recursive);
                }

            // ... redraw this item after all child items have been updated (the visual update pass) ...
            this.onRedraw(false);
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** Changes the type of element to create (if supported by the derived type), and returns the underlying graph instance.
        * Changing this after a layout pass has already created elements will cause the existing element for this graph item to be deleted and replaced.
        * WARNING: This is not supported by all derived types, and calling this AFTER a layout pass has created elements for those unsupported types may have no effect.
        * For example, the UI 'PlaneText' graph item overrides 'createUIElement()' to return a node created from 'document.createTextNode()', 
        * and calling 'setHTMLTag("span")' will have no effect before OR after the first layout pass (this element will always be a text node).
        * Some derived types that override 'createUIElement()' my provide special logic to accept only supported types.
        */
        setHTMLTag(htmlTag: string): GraphItem {
            this.htmlTag = htmlTag;
            // .. if an element already exists, replace it if the tag is different ...
            if (this.element != null && this.element.nodeName != this.htmlTag) {
                this.updateLayout();
            }
            return this;
        }

        /** The function is called in order to produce an HTML element that represents the graph item.
        * The base class, by default, simply returns a new 'HTMLDivElement' element (which doesn't display anything).
        * It is expected that implementers will override this function in derived classes if any custom UI is to be generated.
        * If the derived type doesn't represent a UI element, don't override this function.
        */
        createUIElement(): Node {
            return document.createElement(this.htmlTag);
        }

        /** Call this at the beginning of an overriding 'createUIElement()' function on a derived GraphItem type to validate supported element types. */
        assertSupportedElementTypes(...args: string[]) {
            this.htmlTag = (this.htmlTag || "").toLowerCase();
            //??args = <string[]><any>arguments;
            if (args.length == 1 && typeof args[0] != 'undefined' && typeof args[0] != 'string' && args[0].length) args = <string[]><any>args[0];
            for (var i = 0; i < args.length; i++)
                if (this.htmlTag == args[i]) return true;
            throw DreamSpace.Exception("The element type '" + this.htmlTag + "' is not supported for this GraphItem type.");
        }

        /** Call this at the beginning of an overriding 'createUIElement()' function on a derived GraphItem type to validate unsupported element types. */
        assertUnsupportedElementTypes(...args: string[]) {
            this.htmlTag = (this.htmlTag || "").toLowerCase();
            //??args = <string[]><any>arguments;
            if (args.length == 1 && typeof args[0] != 'undefined' && typeof args[0] != 'string' && args[0].length) args = <string[]><any>args[0];
            for (var i = 0; i < args.length; i++)
                if (this.htmlTag == args[i])
                    throw DreamSpace.Exception("The element type '" + this.htmlTag + "' is not supported for this GraphItem type.");
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** Updates the visual display of all child graph items (@see GraphItem.updateLayout). 
        * Expected to be overridden by derived types that store their layout/css information in the observable properties. 
        * This function is called when properties are completely replaced (such as when loading a project), rather than
        * calling an event for each property set, which can be much slower.
        * Typically, derived types simply update their CSS, innerHTML, or both, in this function.
        */
        onRedraw(recursive: bool = true) {
            if (recursive)
                for (var i = 0; i < this.children.length; i++)
                    this.children[i].onRedraw();
        }

        // -------------------------------------------------------------------------------------------------------------------
        // LinqJS Support

        /** Returns an enumerable that can be used to iterate over the child graph items. */
        asEnumerable(): linqjs.Enumerable {
            return Enumerable.from(this.children);
        }

        // -------------------------------------------------------------------------------------------------------------------
    }

    // =======================================================================================================================

    /** An application object encapsulates the configuration for a single application project. */
    export class Application extends GraphItem {

        // -------------------------------------------------------------------------------------------------------------------

        /** A title for the application (required). */
        title: string;

        /** A short application description (optional). */
        description: string;

        /** This is the target element that the application will render to when updateLayout() is called. */
        private targetElement: HTMLElement;
        getTargetElement(): HTMLElement { return this.targetElement; }

        // -------------------------------------------------------------------------------------------------------------------

        constructor(title: string, description: string, targetElement: HTMLElement) {
            if (typeof title == "undefined" || "" + title == "")
                throw "An application title is required.";
            super(null);
            this.title = title;
            this.description = description;
            this.targetElement = targetElement;
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** This generates/updates the HTML elements required to display the application. */
        updateLayout(recursive: bool = true) {
            super.updateLayout(recursive);

            if (this.element != null) {
                var node = <HTMLElement>this.targetElement.firstChild;
                if (node == null)
                    this.targetElement.appendChild(this.element);
                else
                    node.replaceNode(this.element);
            }
        }

        // -------------------------------------------------------------------------------------------------------------------
    }

    // =======================================================================================================================

    /** An application object encapsulates the configuration for a single application project. */
    export class DreamSpaceSystem {

        // -------------------------------------------------------------------------------------------------------------------

        /** The name of the environment */
        environmentName() { return this._EnvironmentName; }
        private _EnvironmentName: string;

        /** A list of applications in the system. Each application renders to a different target element.
        * Do not add applications directly to this array.  Use the 'createApplication()' function.
        * To remove an application, use the 'removeApplication()' function.
        */
        getApplications() { return this._Applications; }
        private _Applications: Application[] = [];

        /** Returns true if 'host' in the global scope represents the DreamSpace Studio .NET environment. */
        isDreamSpaceStudioEnv(): bool { return this.hasHost() && host.isDreamSpaceStudio(); }

        /** Returns true if 'host' in the global scope represents the DreamSpace Server .NET environment. */
        isDreamSpaceServerEnv(): bool { return this.hasHost() && host.isDreamSpaceServer(); }

        /** Returns true if the currently executing JavaScript is running in an environment that contains a valid DreamSpace
        * host bridge.  If this is false, then DreamSpace has no access to the host (usually a .NET environment, such as
        * DreamSpace Studio or DreamSpace Server).
        */
        hasHost(): bool {
            return typeof host != "undefined"
                && typeof host.isDreamSpaceStudio != "undefined"
                && typeof host.isDreamSpaceServer != "undefined";
        }

        // -------------------------------------------------------------------------------------------------------------------

        constructor(environmentName: string = "DreamSpace Client") {
            this._EnvironmentName = environmentName;
        }

        // -------------------------------------------------------------------------------------------------------------------

        private validateElementTarget(appTitle: string, targetElement: HTMLElement) {
            for (var i = 0; i < this._Applications.length; i++)
                if (this._Applications[i].element == targetElement)
                    throw "Cannot add application '" + appTitle + "' as another application exists with the same target element.  Two applications cannot render to the same target.";
        }

        // -------------------------------------------------------------------------------------------------------------------

        //** Creates a new 'DreamSpace.Application' instance. */
        createApplication(title: string, description: string, targetElement: HTMLElement): Application {
            this.validateElementTarget(title, targetElement);
            var app = new Application(title, description, targetElement);
            this._Applications.push(app);
            return app;
        }

        //** Adds a 'DreamSpace.Application' type instance to the system. */
        addApplication(app: Application): Application {
            this.validateElementTarget(app.title, app.getTargetElement());
            this._Applications.push(app);
            return app;
        }

        //** Removes an existing DreamSpace application. */
        removeApplication(app: Application) {
            var i = this._Applications.indexOf(app);
            if (i > 0) this._Applications.splice(i, 1); // TODO: Terminate/stop app before removing.
        }

        // -------------------------------------------------------------------------------------------------------------------
    }

    // =======================================================================================================================
}

// =======================================================================================================================
// Extend features of GraphItem for Array and JQuery.

(function () {
    /** Extends an existing TypeScript class to contain other prototypes for special cases. 
    * (note: only works once to extend non-extended classes)
    */
    function __extend(type: Function, proto: { constructor: Object }) {
        function __() { this.constructor = proto.constructor; }
        __.prototype = proto;
        // ... copy any already defined properties..
        var newProto = new __();
        for (var p in type.prototype)
            if (type.prototype.hasOwnProperty(p))
                newProto[p] = type.prototype[p];
        type.prototype = newProto;
    };

    // (allow the 'children' array to inherit some JQuery functionality [such as 'each()'])
    if (typeof jQuery != "undefined" && jQuery.fn)
        __extend(DreamSpace.GraphItem, jQuery.fn);
    
    // (give array functionality)
    //for (var p in Array.prototype)
    //    if (Array.prototype.hasOwnProperty(p))
    //        DreamSpace.GraphItem.prototype[p] = Array.prototype[p];
})();

DreamSpace["parent"] = this; // (need a reference to the global object from nested code)

// =======================================================================================================================

var $DS = <DreamSpace.DreamSpaceSystem><any>{
    run() { // (this is only available via the console or when forcibly executed and will not show in intellisense)
        DreamSpace.setFunctionNames(DreamSpace["parent"], "DreamSpace"); // (need to do this here to make sure to capture any static functions set later on 'DreamSpace')
        $DS = new DreamSpace.DreamSpaceSystem();
        $ICE.loadLibraries(); // (load all ICE libraries after the DreamSpace system is ready, but before the ICE libraries are loaded so proper error details can be displayed if required)
        // (any errors before this point will terminate the 'onReady()' callback)
        if (DreamSpace.onReady != null) DreamSpace.onReady();
    },
    onLoaded() {
        if (!host.isDebugMode() && !DreamSpace.debug) (<any>$DS).run();
    }
};

// =======================================================================================================================
